Skip to content

feat: add CI mode to sql-tap client for automated N+1 and slow query detection#26

Merged
mickamy merged 6 commits intomainfrom
feat/ci
Mar 11, 2026
Merged

feat: add CI mode to sql-tap client for automated N+1 and slow query detection#26
mickamy merged 6 commits intomainfrom
feat/ci

Conversation

@mickamy
Copy link
Copy Markdown
Owner

@mickamy mickamy commented Mar 11, 2026

No description provided.

Copilot AI review requested due to automatic review settings March 11, 2026 01:18
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a non-interactive “CI mode” to the sql-tap client so it can run in automated environments, collect query events until termination, then output a summary report and exit with a failure code when N+1/slow queries are detected.

Changes:

  • Add --ci flag to switch between TUI monitoring and CI collection/reporting.
  • Introduce new ci package to stream events from the TapService, aggregate N+1/slow-query problems, and render a report.
  • Add unit tests for CI aggregation and report formatting.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
main.go Adds --ci flag and SIGINT/SIGTERM-driven CI execution path.
ci/ci.go Implements CI streaming, aggregation, and report formatting for detected problems.
ci/ci_test.go Adds unit tests covering aggregation, reporting, and edge cases.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread ci/ci.go Outdated
Comment on lines +116 to +122
// gRPC wraps context errors in status; unwrap and check.
if s, ok := status.FromError(err); ok {
msg := s.Message()
return strings.Contains(msg, "context canceled") ||
strings.Contains(msg, "context deadline exceeded")
}
return false
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isStreamDone determines cancellation/deadline by substring-matching status.FromError(err).Message(). This is brittle (message text can change, be wrapped, or be localized) and can cause CI runs to fail with an error when they should exit cleanly on SIGINT/SIGTERM. Prefer checking status.Code(err) for codes.Canceled / codes.DeadlineExceeded (and/or errors.Is(err, context.Canceled)), and consider updating the server-side Watch handler to return those gRPC status codes on context cancellation so the client can reliably detect stream termination.

Copilot uses AI. Check for mistakes.
Comment thread ci/ci.go
if nq := e.GetNormalizedQuery(); nq != "" {
return nq
}
return e.GetQuery()
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

normalizedOrRaw falls back to emitting the raw SQL when NormalizedQuery is empty. Raw queries may include literal values (PII/secrets) and this CI report is likely to be stored in build logs. Elsewhere (e.g., TUI analytics/export) the code avoids using raw queries when normalization is missing. Consider normalizing locally (e.g., via the existing query.Normalize) or redacting/omitting the query text when a normalized form isn't available, to avoid leaking sensitive data in CI output.

Suggested change
return e.GetQuery()
// Avoid emitting raw SQL, which may contain PII/secrets, in CI output.
return "<query omitted>"

Copilot uses AI. Check for mistakes.
Comment thread ci/ci.go
Comment on lines +92 to +107
func collect(ctx context.Context, stream tapv1.TapService_WatchClient) (Result, error) {
var events []*tapv1.QueryEvent

for {
resp, err := stream.Recv()
if err != nil {
if isStreamDone(ctx, err) {
break
}
return Result{}, fmt.Errorf("recv: %w", err)
}
events = append(events, resp.GetEvent())
}

return Aggregate(events), nil
}
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

collect appends every received event into an in-memory slice and only aggregates at the end. In CI mode this runs until SIGINT/SIGTERM, so memory usage can grow without bound and potentially OOM on long/verbose runs. Consider aggregating incrementally while streaming (update grouped stats per event) and only keep the aggregates needed for the final report, rather than storing all events.

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings March 11, 2026 01:28
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 5 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread ci/ci.go Outdated
}

// Run connects to the gRPC server at addr, collects query events until ctx is
// cancelled, and returns the aggregated result.
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Run doc comment says it "collects query events until ctx is cancelled", but collect also stops on io.EOF (server-side stream completion). Please adjust the comment so it matches the behavior (cancellation or stream end).

Suggested change
// cancelled, and returns the aggregated result.
// cancelled or the server closes the stream, and returns the aggregated result.

Copilot uses AI. Check for mistakes.
Comment thread ci/ci.go
fmt.Fprintf(&b, " [N+1] %s (detected %d times)\n", p.Query, p.Count)
case ProblemSlowQuery:
avg := p.AvgDuration.Truncate(time.Millisecond)
fmt.Fprintf(&b, " [SLOW] %s (avg %s, %d occurrences)\n", p.Query, avg, p.Count)
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Result.Report() silently ignores Problem.Kind values other than N+1/SLOW (no default case), which can produce a "Problems found" section with missing entries since Problem/ProblemKind are exported and can be constructed externally. Consider adding a default case (or an explicit unknown formatter) so reports stay informative even if new/invalid kinds appear.

Suggested change
fmt.Fprintf(&b, " [SLOW] %s (avg %s, %d occurrences)\n", p.Query, avg, p.Count)
fmt.Fprintf(&b, " [SLOW] %s (avg %s, %d occurrences)\n", p.Query, avg, p.Count)
default:
fmt.Fprintf(&b, " [%s] %s (%d occurrences)\n", string(p.Kind), p.Query, p.Count)

Copilot uses AI. Check for mistakes.
Comment thread ci/ci.go
Comment on lines +75 to +91
// Run connects to the gRPC server at addr, collects query events until ctx is
// cancelled, and returns the aggregated result.
func Run(ctx context.Context, addr string) (Result, error) {
conn, err := grpc.NewClient(addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
return Result{}, fmt.Errorf("dial %s: %w", addr, err)
}
defer func() { _ = conn.Close() }()

client := tapv1.NewTapServiceClient(conn)
stream, err := client.Watch(ctx, &tapv1.WatchRequest{})
if err != nil {
return Result{}, fmt.Errorf("watch %s: %w", addr, err)
}

return collect(ctx, stream)
}
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The streaming path (Run/collect/isStreamDone) isn’t covered by tests in this package. Since CI mode relies on correct cancellation/EOF handling and error classification, consider adding an integration-style test that starts an in-process gRPC TapService (similar to server/server_test.go) and asserts that ci.Run returns aggregated results and exits cleanly on context cancellation.

Copilot uses AI. Check for mistakes.
Comment thread main.go Outdated
return 1
}

fmt.Fprint(os.Stderr, result.Report())
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

-ci mode prints the report to stderr. In CI environments this can be treated as an error stream even when the run succeeds, and it also makes it harder to pipe/report the output. Consider printing the report to stdout and reserving stderr for actual errors.

Suggested change
fmt.Fprint(os.Stderr, result.Report())
fmt.Fprint(os.Stdout, result.Report())

Copilot uses AI. Check for mistakes.
Comment thread main.go Outdated
}

showVersion := fs.Bool("version", false, "show version and exit")
ciMode := fs.Bool("ci", false, "run in CI mode: collect events until SIGTERM/SIGINT, then report and exit")
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The -ci flag help text says it runs "until SIGTERM/SIGINT", but the implementation will also exit if the gRPC stream ends (e.g., server stops or connection drops). Please update the help string to reflect the actual termination conditions to avoid confusing CI users.

Suggested change
ciMode := fs.Bool("ci", false, "run in CI mode: collect events until SIGTERM/SIGINT, then report and exit")
ciMode := fs.Bool("ci", false, "run in CI mode: collect events until SIGTERM/SIGINT or the stream ends, then report and exit")

Copilot uses AI. Check for mistakes.
mickamy added 2 commits March 11, 2026 10:34
- Update doc comment and help text to mention stream-end termination
- Print CI report to stdout instead of stderr
- Add default case in Report for unknown ProblemKind
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@mickamy mickamy merged commit 272ec8c into main Mar 11, 2026
5 checks passed
@mickamy mickamy deleted the feat/ci branch March 11, 2026 06:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants